﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;

namespace BZLib
{
    namespace Network
    {
        public delegate void ServerDataCallback(NetMessage msg, IMessageDataReceived data);

        public class ServerDisconnectedException : Exception { }

        public class ServerConnection
        {
            private TcpClient client;
            private NetworkStream stream;
            private Thread readThread;
            public string Hostname { get; set; }
            public int Port { get; set; }
            public ServerDataCallback Callbacks;

            public ServerConnection()
            {
                client = new TcpClient();
            }

            public void Connect(string hostname, int port)
            {
                try
                {
                    Console.WriteLine("Connecting...");
                    client.Connect(hostname, port);
                    stream = client.GetStream();

                    byte[] buf = new byte[9];
                    stream.Read(buf, 0, 9);

                    BinaryString buffer = new BinaryString(buf);
                    string magic = buffer.UnpackString(4);
                    string protocol = buffer.UnpackString(4);
                    // Ignore the last byte

                    Console.WriteLine("Magic: {0}, Proto: {1}", magic, protocol);

                    if (!(magic == "BZFS" && protocol == "0026"))
                        throw new Exception("Not a BZFlag Server");

                    readThread = new Thread(new ThreadStart(ReadLoop));
                    readThread.Start();
                }
                catch (Exception)
                {
                    throw new Exception("Unable to connect to server.");
                }
            }

            public void Disconnect()
            {
                try
                {
                    stream.Close();
                }
                catch(Exception)
                {
                }
            }

            public void Send(IMessageDataSent info)
            {
                BinaryString data = info.Pack();

                data = new BinaryString().PackUShort((ushort)data.Length)
                    .PackUShort((ushort)info.GetMessageType())
                    .Add(data);

                Console.WriteLine("Sending data: {0}", data.ToString());

                try
                {
                    stream.Write(data.ToArray(), 0, data.Length);
                }
                catch
                {
                    //throw new ServerDisconnectedException();
                }
            }

            private void ReadLoop()
            {
                Console.WriteLine("Starting read loop.");

                while (client.Client.Connected)
                {
                    if (stream.DataAvailable)
                    {
                        try
                        {
                            byte[] header = new byte[4];
                            stream.Read(header, 0, 4);
                            BinaryString buffer = new BinaryString(header);

                            int len = buffer.UnpackUShort();
                            NetMessage msg = (NetMessage)buffer.UnpackUShort();
                            Console.WriteLine("Received Message {0} with a length of {1}", msg.ToString(), len);

                            byte[] body = new byte[len];
                            int bytesRead = 0;

                            while (bytesRead != len)
                            {
                                bytesRead += stream.Read(body, bytesRead, len - bytesRead);
                            }

                            Console.Write(new BinaryString(body).ToString());
                            IMessageDataReceived data = Unpack(msg, new BinaryString(body));
                            if (data != null)
                            {
                                if (Callbacks != null)
                                {
                                    Callbacks(msg, data);
                                }
                            }
                        }
                        catch (Exception)
                        {
                            // Socket closed on us.
                            Disconnect();
                            Console.WriteLine("Lost connection to server.");
                            return;
                        }
                    }
                    else
                    {
                        Thread.Sleep(1); // Wait 1ms so we don't eat up a ton of cpu
                    }
                }
            }

            public static Dictionary<NetMessage, IMessageDataReceived> NetMessageUnpackers = new Dictionary<NetMessage, IMessageDataReceived>()
            {
                { NetMessage.Accept, new AcceptDataReceived() },
                { NetMessage.AddPlayer, new AddPlayerDataReceived() },
                { NetMessage.AdminInfo, new AdminInfoDataReceived() },
                { NetMessage.Alive, new AliveDataReceived() },
                { NetMessage.AutoPilot, new AutoPilotDataReceived() },
                { NetMessage.CacheURL, new CacheURLDataReceived() },
                { NetMessage.CaptureFlag, new CaptureFlagDataReceived() },
                { NetMessage.ChatMessage, new ChatMessageDataReceived() },
                { NetMessage.CustomSound, new CustomSoundDataReceived() },
                { NetMessage.DropFlag, new DropFlagDataReceived() },
                { NetMessage.FetchResources, new FetchResourcesDataReceived() },
                { NetMessage.FlagUpdate, new FlagUpdateReceivedData() },
                { NetMessage.GameSettings, new GameSettingsDataReceived() },
                { NetMessage.GameTime, new GameTimeDataReceived() },
                { NetMessage.GetWorld, new GetWorldDataReceived() },
                { NetMessage.GMUpdate, new GMUpdateDataReceived() },
                { NetMessage.GrabFlag, new GrabFlagDataReceived() },
                { NetMessage.Killed, new KilledDataReceived() },
                { NetMessage.LagPing, new LagPingDataReceived() },
                { NetMessage.NegotiateFlags, new NegotiateFlagsDataReceived() },
                { NetMessage.NewRabbit, new NewRabbitDataReceived() },
                { NetMessage.Pause, new PauseDataReceieved() },
                { NetMessage.PlayerInfo, new PlayerInfoDataReceived() },
                { NetMessage.PlayerUpdate, new PlayerUpdateDataReceived() },
                { NetMessage.PlayerUpdateSmall, new PlayerUpdateSmallDataReceived() },
                { NetMessage.Reject, new RejectDataReceived() },
                { NetMessage.RemovePlayer, new RemovePlayerDataReceived() },
                { NetMessage.ReplayReset, new ReplayResetDataReceived() },
                { NetMessage.Score, new ScoreDataReceived() },
                { NetMessage.ScoreOver, new ScoreOverEventDataReceived() },
                { NetMessage.SetVar, new SetVarEventDataReceived() },
                { NetMessage.ShotBegin, new ShotBeginDataReceived() },
                { NetMessage.ShotEnd, new ShotEndDataReceived() },
                { NetMessage.SuperKill, new SuperKillDataReceived() },
                { NetMessage.TeamUpdate, new TeamUpdateDataReceived() },
                { NetMessage.Teleport, new TeleportDataReceived() },
                { NetMessage.TimeUpdate, new TimeUpdateDataReceived() },
                { NetMessage.TransferFlag, new TransferFlagDataReceived() }
            };

            private IMessageDataReceived Unpack(NetMessage msg, BinaryString data)
            {
                if (!NetMessageUnpackers.ContainsKey(msg))
                {
                    throw new Exception("Unknown message type " + msg);
                }

                IMessageDataReceived receiver = NetMessageUnpackers[msg];
                receiver.Unpack(data);
                return receiver;
            }
        }
    }
}
